// This file is part of Keepass2Android, Copyright 2025 Philipp Crocoll.
//
//   Keepass2Android is free software: you can redistribute it and/or modify
//   it under the terms of the GNU General Public License as published by
//   the Free Software Foundation, either version 3 of the License, or
//   (at your option) any later version.
//
//   Keepass2Android is distributed in the hope that it will be useful,
//   but WITHOUT ANY WARRANTY; without even the implied warranty of
//   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
//   GNU General Public License for more details.
//
//   You should have received a copy of the GNU General Public License
//   along with Keepass2Android.  If not, see <http://www.gnu.org/licenses/>.

using System;
using System.Collections.Generic;
using Android.Content;
using Android.OS;
using Android.Preferences;
using Android.Widget;
using keepass2android;
using keepass2android;

namespace PluginTOTP
{
  class TrayTotpPluginAdapter : ITotpPluginAdapter
  {
    public TotpData GetTotpData(IDictionary<string, string> entryFields, Context ctx, bool muteWarnings)
    {
      return new TrayTotpHandler(ctx, new Handler(Looper.MainLooper), muteWarnings).GetTotpData(entryFields);
    }

    private class TrayTotpHandler
    {
      private readonly Context _ctx;
      private readonly Handler _uiThreadHandler;
      private readonly bool _muteWarnings;

      private string SeedFieldName { get { return PreferenceManager.GetDefaultSharedPreferences(_ctx).GetString(_ctx.GetString(Resource.String.TrayTotp_SeedField_key), "TOTP Seed"); } }
      private string SettingsFieldName { get { return PreferenceManager.GetDefaultSharedPreferences(_ctx).GetString(_ctx.GetString(Resource.String.TrayTotp_SettingsField_key), "TOTP Settings"); } }

      public TrayTotpHandler(Context ctx, Handler uiThreadHandler, bool muteWarnings)
      {
        _ctx = ctx;
        _uiThreadHandler = uiThreadHandler;
        _muteWarnings = muteWarnings;
      }


      internal bool HasSeed(IDictionary<string, string> entryFields)
      {
        string seed;
        entryFields.TryGetValue(SeedFieldName, out seed);
        return !String.IsNullOrEmpty(seed);
      }

      /// <summary>
      /// Check if specified Entry's Interval and Length are valid. All settings statuses are available as out booleans.
      /// </summary>
      /// <param name="pe">Password Entry.</param>
      /// <param name="IsIntervalValid">Interval Validity.</param>
      /// <param name="IsLengthValid">Length Validity.</param>
      /// <param name="IsUrlValid">Url Validity.</param>
      /// <returns>Error(s) while validating Interval or Length.</returns>
      internal bool SettingsValidate(IDictionary<string, string> entryFields, out bool IsIntervalValid, out bool IsLengthValid, out bool IsUrlValid)
      {
        bool SettingsValid = true;
        try
        {
          string[] Settings = SettingsGet(entryFields);
          try
          {
            IsIntervalValid = (Convert.ToInt16(Settings[0]) > 0) && (Convert.ToInt16(Settings[0]) < 61); //Interval
          }
          catch (Exception)
          {
            IsIntervalValid = false;
            SettingsValid = false;
          }
          try
          {
            IsLengthValid = (Settings[1] == "6") || (Settings[1] == "8") || (Settings[1] == "S"); //Length
          }
          catch (Exception)
          {
            IsLengthValid = false;
            SettingsValid = false;
          }
          try
          {
            IsUrlValid = (Settings[2].StartsWith("http://")) || (Settings[2].StartsWith("https://")); //Url
          }
          catch (Exception)
          {
            IsUrlValid = false;
          }
        }
        catch (Exception)
        {
          IsIntervalValid = false;
          IsLengthValid = false;
          IsUrlValid = false;
          SettingsValid = false;
        }
        return SettingsValid;
      }

      private string[] SettingsGet(IDictionary<string, string> entryFields)
      {
        return entryFields.TryGetValue(SettingsFieldName, out var settings) ? settings.Split(';') : new[] { "30", "6" };
      }

      public TotpData GetTotpData(IDictionary<string, string> entryFields)
      {
        TotpData res = new TotpData();

        if (HasSeed(entryFields))
        {
          bool ValidInterval; bool ValidLength; bool ValidUrl;
          if (SettingsValidate(entryFields, out ValidInterval, out ValidLength, out ValidUrl))
          {
            bool NoTimeCorrection = false;
            string[] Settings = SettingsGet(entryFields);
            res.InternalFields.Add(SettingsFieldName);
            res.InternalFields.Add(SeedFieldName);
            res.Duration = Settings[0];
            res.Length = Settings[1];
            if (res.Length == "S")
              res.Encoder = TotpData.EncoderSteam;
            if (ValidUrl)
            {
              NoTimeCorrection = true;
              res.TimeCorrectionUrl = Settings[2];
              /*var CurrentTimeCorrection = TimeCorrections[Settings[2]];
              if (CurrentTimeCorrection != null)
              {
                  TotpGenerator.TimeCorrection = CurrentTimeCorrection.TimeCorrection;
              }
              else
              {
                  TotpGenerator.TimeCorrection = TimeSpan.Zero;
                  NoTimeCorrection = true;
              }*/
            }
            string InvalidCharacters;
            if (SeedValidate(entryFields, out InvalidCharacters))
            {
              res.IsTotpEntry = true;
              res.TotpSeed = SeedGet(entryFields).ExtWithoutSpaces();


            }
            else
            {
              ShowWarning("Bad seed!" + InvalidCharacters.ExtWithParenthesis().ExtWithSpaceBefore());
            }
            if (NoTimeCorrection)
              ShowWarning("Warning: TOTP Time correction not implemented!");
          }
          else
          {
            ShowWarning("Bad settings!");
          }
        }
        else
        {
          //no totp entry
        }
        return res;
      }

      private void ShowWarning(string warning)
      {
        if (_muteWarnings)
          return;
        try
        {
          _uiThreadHandler.Post(() => App.Kp2a.ShowMessage(_ctx, warning, MessageSeverity.Warning));
        }
        catch (Exception e)
        {
          Kp2aLog.LogUnexpectedError(e);
          //ignore, it's only a warning
        }

      }

      private bool SeedValidate(IDictionary<string, string> entryFields, out string invalidCharacters)
      {
        return SeedGet(entryFields).ExtWithoutSpaces().ExtIsBase32(out invalidCharacters);
      }
      internal string SeedGet(IDictionary<string, string> entryFields)
      {
        return entryFields[SeedFieldName];
      }
    }


  }

  /// <summary>
  /// Class to support custom extensions.
  /// </summary>
  internal static class Extensions
  {
    /// <summary>
    /// Concatenates a space in front of the current string.
    /// </summary>
    /// <param name="Extension">Current string.</param>
    /// <returns></returns>
    internal static string ExtWithSpaceBefore(this string Extension)
    {
      return " " + Extension;
    }

    /// <summary>
    /// Concatenates the current string with space to the end.
    /// </summary>
    /// <param name="Extension">Current string.</param>
    /// <returns></returns>
    internal static string ExtWithSpaceAfter(this string Extension)
    {
      return Extension + " ";
    }

    /// <summary>
    /// Concatenates the current string with a bracket in front and to the end.
    /// </summary>
    /// <param name="Extension">Current string.</param>
    /// <returns></returns>
    internal static string ExtWithBrackets(this string Extension)
    {
      return ExtWith(Extension, '{', '}');
    }

    /// <summary>
    /// Concatenates the current string with a parenthesis in front and to the end.
    /// </summary>
    /// <param name="Extension">Current string.</param>
    /// <returns></returns>
    internal static string ExtWithParenthesis(this string Extension)
    {
      return ExtWith(Extension, '(', ')');
    }

    /// <summary>
    /// Concatenates the current string with a charater in front and another character to the end.
    /// </summary>
    /// <param name="Extension">Current string.</param>
    /// <param name="Left">Front character.</param>
    /// <param name="Right">End charater.</param>
    /// <returns></returns>
    internal static string ExtWith(this string Extension, char Left, char Right)
    {
      return Left + Extension + Right;
    }

    /// <summary>
    /// Remove all spaces from the current string.
    /// </summary>
    /// <param name="Extension">Current string.</param>
    /// <returns></returns>
    internal static string ExtWithoutSpaces(this string Extension)
    {
      return Extension.ExtWithout(" ");
    }

    /// <summary>
    /// Remove all specified characters from the current string.
    /// </summary>
    /// <param name="Extension">Current string.</param>
    /// <param name="Chars">Characters to remove.</param>
    /// <returns></returns>
    internal static string ExtWithout(this string Extension, string Chars)
    {
      foreach (var Char in Chars)
      {
        Extension = Extension.Replace(Char.ToString(), "");
      }
      return Extension;
    }



    /// <summary>
    /// Splits the string and returns specified substring.
    /// </summary>
    /// <param name="Extension">Current string.</param>
    /// <param name="Index">Split index.</param>
    /// <param name="Seperator">Split seperators.</param>
    /// <returns></returns>
    internal static string ExtSplit(this string Extension, int Index, char Seperator = ';')
    {
      if (Extension != string.Empty)
      {
        try
        {
          var Text = Extension;
          if (Text.Contains(Seperator.ToString()))
          {
            return Text.Split(Seperator)[Index];
          }
          return Text;
        }
        catch (Exception)
        {
          return string.Empty;
        }
      }
      return string.Empty;
    }

    /// <summary>
    /// Makes sure the string provided as a Seed is Base32. Invalid characters are available as out string.
    /// </summary>
    /// <param name="Extension">Current string.</param>
    /// <param name="InvalidChars">Invalid characters.</param>
    /// <returns>Validity of the string's characters for Base32 format.</returns>
    internal static bool ExtIsBase32(this string Extension, out string InvalidChars)
    {
      InvalidChars = null;
      try
      {
        foreach (var CurrentChar in Extension)
        {
          var CurrentCharValue = Char.GetNumericValue(CurrentChar);
          if (Char.IsLetter(CurrentChar))
          {
            continue;
          }
          if (Char.IsDigit(CurrentChar))
          {
            if ((CurrentCharValue > 1) && (CurrentCharValue < 8))
            {
              continue;
            }
          }
          InvalidChars = (InvalidChars + CurrentCharValue.ToString().ExtWithSpaceBefore()).Trim();
        }
      }
      catch (Exception)
      {
        InvalidChars = "(error)";
      }
      return InvalidChars == null;
    }
  }
}